package com.kdgc.energy.dmm.query;

import cn.hutool.core.collection.CollectionUtil;
import com.kdgc.energy.dmm.Environment;
import com.kdgc.energy.dmm.condition.Condition;
import com.kdgc.energy.dmm.groupby.GroupBy;
import com.kdgc.energy.dmm.join.Join;
import com.kdgc.energy.dmm.join.Join0;
import com.kdgc.energy.dmm.lambda.SerializedFunction;
import com.kdgc.energy.dmm.result.QueryResult;
import com.kdgc.energy.dmm.table.Table;
import com.kdgc.energy.dmm.time.TimeStatistical;
import com.kdgc.energy.dmm.util.NameUtil;
import lombok.Getter;
import org.apache.metamodel.data.DataSet;
import org.apache.metamodel.jdbc.JdbcDataContext;
import org.apache.metamodel.query.JoinType;
import org.apache.metamodel.query.*;
import org.apache.metamodel.schema.Column;
import org.apache.metamodel.schema.MutableColumn;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 查询
 *
 * @author xu.wenchang
 * @version 1.0 2022/04/16
 */
public class Query {
    @Getter
    protected org.apache.metamodel.query.Query query;
    
    private List<Join0> joins;
    
    private boolean selectAll;
    
    private TimeStatistical timeStatistical;
    
    public Query() {
        this.query = new org.apache.metamodel.query.Query();
        this.joins = new ArrayList<>();
        this.timeStatistical = new TimeStatistical();
        this.timeStatistical.phaseStart("build");
    }
    
    /**
     * 字段选择块
     */
    
    public Query select() {
        return this.selectAll();
    }
    
    public Query selectAll() {
        this.selectAll = true;
        return this;
    }
    
    public Query select(Class<?> clazz) {
        Table table = Environment.get(clazz);
        this.query.select(table.getTable()
                               .getColumns());
        return this;
    }
    
    public <T1, R1, T2, R2, T3, R3> Query select(SerializedFunction<T1, R1> sf1, SerializedFunction<T2, R2> sf2, SerializedFunction<T3, R3> sf3) {
        List<Column> columns = new ArrayList<>();
        columns.add(Environment.get(sf1));
        columns.add(Environment.get(sf2));
        columns.add(Environment.get(sf3));
        this.query.select(columns);
        return this;
    }
    
    @SafeVarargs
    public final <T, R> Query select(SerializedFunction<T, R>... sfs) {
        return select(Arrays.asList(sfs));
    }
    
    public <T, R> Query select(List<SerializedFunction<T, R>> list) {
        assert CollectionUtil.isNotEmpty(list) : "字段不能为空";
        List<Column> columns = list.stream()
                                   .map(Environment::get)
                                   .collect(Collectors.toList());
        this.query.select(columns);
        return this;
    }
    
    public <T, R> Query select(SerializedFunction<T, R> sf, From fromItem) {
        this.query.select(Environment.get(sf), fromItem.getDelegate());
        return this;
    }
    
    public Query select(Select... items) {
        assert null != items && items.length > 0 : "不能传入null";
        SelectItem[] arr = Arrays.stream(items)
                                 .map(Select::getDelegate)
                                 .toArray(SelectItem[]::new);
        this.query.select(arr);
        return this;
    }
    
    public Query selectAll(final From fromItem) {
        this.query.selectAll(fromItem.getDelegate());
        return this;
    }
    
    /**
     * 函数块
     */
    
    public <T, R> Query max(SerializedFunction<T, R> sf) {
        this.query.select(FunctionType.MAX, Environment.get(sf));
        return this;
    }
    
    public <T, R> Query min(SerializedFunction<T, R> sf) {
        this.query.select(FunctionType.MIN, Environment.get(sf));
        return this;
    }
    
    public <T, R> Query count(SerializedFunction<T, R> sf) {
        this.query.select(FunctionType.COUNT, Environment.get(sf));
        return this;
    }
    
    public Query count() {
        this.query.selectCount();
        return this;
    }
    
    public Query count(String alias) {
        SelectItem si = new SelectItem(FunctionType.COUNT, "*", alias);
        this.query.select(si);
        return this;
    }
    
    public <T, R> Query average(SerializedFunction<T, R> sf) {
        this.query.select(FunctionType.AVG, Environment.get(sf));
        return this;
    }
    
    public <T, R> Query sum(SerializedFunction<T, R> sf) {
        this.query.select(FunctionType.SUM, Environment.get(sf));
        return this;
    }
    
    public <T, R> Query average(SerializedFunction<T, R> sf, String alias) {
        SelectItem si = new SelectItem(FunctionType.AVG, Environment.getColumnName(sf), alias);
        this.query.select(si);
        return this;
    }
    
    public <T, R> Query sum(SerializedFunction<T, R> sf, String alias) {
        SelectItem si = new SelectItem(FunctionType.SUM, Environment.getColumnName(sf), alias);
        this.query.select(si);
        return this;
    }
    
    public <T, R> Query distinct(SerializedFunction<T, R> sf) {
        this.query.select(new SelectItem(Environment.get(sf)));
        this.query.selectDistinct();
        return this;
    }
    
    public Query distinct() {
        this.query.selectDistinct();
        return this;
    }
    
    /**
     * from块
     */
    
    public Query from(From... items) {
        FromItem[] arr = Arrays.stream(items)
                               .map(From::getDelegate)
                               .toArray(FromItem[]::new);
        this.query.from(arr);
        return this;
    }
    
    public Query from(Class<?>... entityClasses) {
        for (Class<?> clazz : entityClasses) {
            from(Environment.get(clazz));
        }
        
        return this;
    }
    
    public Query from(Class<?> entityClass) {
        return from(Environment.get(entityClass));
    }
    
    public Query from(Class<?> entityClass, String alias) {
        return from(Environment.get(entityClass), alias);
    }
    
    public Query from(Table table) {
        this.query.from(table.getTable());
        return this;
    }
    
    public Query from(Table table, String alias) {
        this.query.from(table.getTable(), alias);
        return this;
    }
    
    public Query join(Join join) {
        if (null != join.getFromItem()) {
            this.query.from(join.getFromItem());
        } else {
            this.joins.add(join.getJoin0());
        }
        
        return this;
    }
    
    public <T, R> Query groupBy(SerializedFunction<T, R> sf) {
        this.query.groupBy(Environment.get(sf));
        return this;
    }
    
    public Query groupBy(GroupBy groupBy) {
        this.query.groupBy(groupBy.getArr());
        return this;
    }
    
    public Query having(Condition condition) {
        this.query.having(condition.getDelegate());
        return this;
    }
    
    /**
     * 排序块
     */
    
    public Query orderBy(Select select) {
        this.query.orderBy(new OrderByItem(select.getDelegate()));
        return this;
    }
    
    public Query orderBy(Select select, Sort sort) {
        this.query.orderBy(new OrderByItem(select.getDelegate(), Sort.ASC == sort ? OrderByItem.Direction.ASC : OrderByItem.Direction.DESC));
        return this;
    }
    
    public <T, R> Query orderBy(SerializedFunction<T, R> sf) {
        this.orderBy(sf, Sort.ASC);
        return this;
    }
    
    public <T, R> Query orderBy(SerializedFunction<T, R> sf, Sort sort) {
        this.query.orderBy(Environment.get(sf), sort == Sort.ASC ? OrderByItem.Direction.ASC : OrderByItem.Direction.DESC);
        return this;
    }
    
    /**
     * where块
     */
    
    public Query where(Condition condition) {
        this.query.where(condition.getDelegate());
        return this;
    }
    
    private void buildJoinStatements() {
        if (this.joins.size() > 0) {
            if (this.joins.size() == 1) {
                Join0 first = this.joins.get(0);
                this.query.from(first.getLeftCol()
                                     .getTable(), first.getRightCol()
                                                       .getTable(), first.getJoinType(), first.getLeftCol(), first.getRightCol());
            } else {
                final Set<String> tables = new HashSet<>();
                final StringBuilder builder = new StringBuilder();
                String firstTableName = this.joins.get(0)
                                                  .getLeftCol()
                                                  .getTable()
                                                  .getQualifiedLabel();
                builder.append(firstTableName);
                tables.add(firstTableName);
                
                this.joins.forEach(join -> {
                    MutableColumn col;
                    
                    if (tables.contains(join.getLeftCol()
                                            .getTable()
                                            .getQualifiedLabel())) {
                        col = join.getRightCol();
                    } else {
                        col = join.getLeftCol();
                    }
                    
                    tables.add(col.getTable()
                                  .getQualifiedLabel());
                    
                    if (join.getJoinType() == JoinType.INNER) {
                        builder.append(" INNER JOIN");
                    } else if (join.getJoinType() == JoinType.LEFT) {
                        builder.append(" LEFT JOIN");
                    } else {
                        builder.append(" RIGHT JOIN");
                    }
                    
                    builder.append(" ");
                    builder.append(col.getTable()
                                      .getQualifiedLabel());
                    builder.append(" ON ");
                    builder.append(NameUtil.getColumnName(join.getLeftCol()));
                    builder.append(" = ");
                    builder.append(NameUtil.getColumnName(join.getRightCol()));
                });
                
                this.query.from(builder.toString());
            }
        }
    }
    
    public QueryResult execute() {
        this.buildJoinStatements();
        
        if (this.selectAll) {
            this.query.selectAll();
        }
        
        if (Environment.isShowSQL()) {
            Environment.getWriter()
                       .write(this.query.toSql());
        }
        
        JdbcDataContext ctx = new JdbcDataContext(Environment.getDataSource());
        CompiledQuery cq = ctx.compileQuery(this.query);
        this.timeStatistical.phaseEnd("build");
        this.timeStatistical.phaseStart("execute");
        DataSet dataSet = ctx.executeQuery(cq);
        this.timeStatistical.phaseEnd("execute");
        return new QueryResult(this.timeStatistical, this.query.toSql(), dataSet);
    }
    
    public Query limit(int limit) {
        this.query.setMaxRows(limit);
        return this;
    }
    
    public Query limit(int skip, int limit) {
        this.query.setMaxRows(limit);
        this.query.setFirstRow(skip + 1);
        return this;
    }
    
    public String toSQL() {
        return this.query.toSql();
    }
}
